Wi-Fi 環境下で大量データ取得しアプリ使用時の通信量を最適化する方法を LINE ミニアプリで実装する

Wi-Fi 環境下で大量データ取得しアプリ使用時の通信量を最適化する方法を LINE ミニアプリで実装する

LINEミニアプリで大容量データを事前ダウンロードする検証とデモアプリを作りました
Clock Icon2024.08.02

こんにちは。リテールアプリ共創部のきんじょーです。

皆さんは普段ネイティブアプリを利用する際、アプリで使用するデータのダウンロード実行と、Wi-Fi 環境下での ダウンロード を薦めるダイアログが表示されることはありませんか?
これは初回起動時・更新時などにアプリで使用するデータを一括でダウンロードし、アプリ内で使用するデータを端末にキャッシュして、アプリのデータ通信量とレイテンシーを最適化する 1 つの方法です。

LINE ミニアプリでも同様の手法でデータ通信量を削減できないかと思い、実装してみた結果をこのブログにまとめます。

実装したデモアプリは以下の QR コードから試すことができます。
※一般公開する都合上、LIFF アプリで公開していますが、LINE ミニアプリでも動作確認済みです。

qr-liff-initial-download-with-indexed-db
https://liff.line.me/2005966107-VWMDX5jn

検証用サイトのコードは以下に格納しています。実装の詳細が知りたい方は以下を参照してください。

https://github.com/joe-king-sh/liff-initial-download-with-indexed-db-sample

LINE ミニアプリのストレージ

LINE ミニアプリは LINE のプラットフォーム上で動作する、実質的には Web アプリケーションです。そのため Web ブラウザで利用できるクライアントサイドのストレージが利用できます。

大量データを長期間ブラウザに保存したい場合、Indexed DBCache APIが候補となります。

Web アプリでネイティブアプリのようなキャッシュ管理やオフライン対応、バックグラウンドでのデータ同期を実装する場合、Service Worker と Cache API を利用する方法が一般的です。
しかし、LINE ミニアプリでは LIFF ブラウザの仕様から Service Worker を利用することができません。

Cache API を単体で利用することもできるようですが、大容量のデータの格納場所として、今回は Indexed DB にデータを保存する方法を選択しました。

Indexed DB とは

Indexed DB は、ブラウザ内に構造化データを永続的に保存するための API です。
RDBMS とは違いスキーマレスで JavaScript のオブジェクトをそのまま保存する Key Value Store です。任意の項目にインデックスを貼ることができ、トランザクション制御も対応しています。
他の Web Storage と同様に、Indexed DB に保存したデータは同一オリジンのページからのみアクセス可能です。

保存容量

Indexed DB 自体には、localStorage や sessionStorage のような容量制限はありませんが、保存できる容量はブラウザやデバイスの空き容量に依存します。

ブラウザ毎にストレージ管理システムの仕様が違い、詳細は以下を参照してください。

https://developer.mozilla.org/ja/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria

LIFF ブラウザの Indexed DB の容量を確認

Indexed DB への保存容量を試せるとても便利なサイトがあったので、LINE 内 ブラウザで試してみました。

上記サイトを LIFF 内ブラウザで開いて、25GB のデータを Indexed DB に保存してみます。
indexed-db-test-site

問題なく登録は完了し、以下のような結果が得られました。

Indexed DB 登録前 Indexed DB 登録後
before-iphone-storage after-iphone-storage
before-line-storage after-line-storage
before-line-delete-data after-line-delete-data

iPhone のストレージが 100GB 以上空いている状態であれば、25GB 登録しても問題なく登録完了しました。

少し意外だったのは、LINE の「設定>トーク>データ削除」から開いた容量確認画面で、Indexed DB は「ほかのデータ」としてカウントされていますが、「キャッシュ」の容量には含まれていません。しかし「キャッシュ」を削除すると Indexed DB に保存されたデータも削除されました。

これだけの容量のデータを保存できるのであれば、LINE ミニアプリで事前に使用するデータをダウンロードしておくストレージとして十分採用が検討できます。

一方で、私の環境では問題ありませんでしたが保存容量やデータの生存期間はデバイスと空き容量に依存するため、端末にデータが保存できなかった場合、データが消えた場合のハンドリングは適切にすべき点には注意が必要です。

デモサイトを作ってみた

Indexed DB の操作

Indexed DB の API は低レベルな実装となっており、直接操作をするのは少し手間がかかります。
さまざまなラッパーライブラリが存在しますが、スター数と現在も活発に開発が続いている様子からDexie.jsを利用して実装しました。

初回起動時に動画ファイルを DB して端末に保存する、という仕様と仮定し、動画データを Blob で格納するスキーマを定義しています。

db.ts
import Dexie, { type EntityTable } from "dexie";

type Video = {
  id: string;
  registeredAt: Date;
  blob: Blob;
};

const db = new Dexie("VideosDatabase") as Dexie & {
  videos: EntityTable<
    Video,
    "id" // primary key "id" (for the typings only)
  >;
};

// Schema declaration:
db.version(1).stores({
  videos: "&id", // primary key "id"
});

export type { Video };
export { db };

トップページの実装

トップページでは、まず Indexed DB に動画データが登録されているかを確認し、登録されていない場合は動画データをダウンロードするダイアログを表示します。
ダウンロードが完了した後は Indexed DB に保存された動画データを video タグで再生します。

index.ts
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { DownloadConfirmModal } from "./DownloadConfirmModal";
import { Button } from "@/components/Button";
import { useLogger } from "@/hooks/useLogger";
import { db, type Video } from "@/schemas/db";
import { buildAppTitle } from "@/utils/string";

export const TopPage = (): JSX.Element => {
  const logger = useLogger().logger;

  const [isOpen, setIsOpen] = useState(true);
  const [isDownloading, setIsDownloading] = useState(false);
  const [videoUrl, setVideoUrl] = useState<string | null>(null);

  useEffect(() => {
    const fetchVideo = async () => {
      const video = await db.videos.get("line-mini-app-initial-dl-test");
      if (video != null) {
        const url = URL.createObjectURL(video.blob);
        setVideoUrl(url);
        setIsOpen(false);
      }
    };
    fetchVideo().catch((error) =>
      logger.error({ message: "Error fetching video from IndexedDB:", error })
    );
  }, [logger]);

  const handleDownload = async () => {
    logger.info({ message: "Downloading video..." });
    setIsDownloading(true);
    try {
      const response = await fetch(
        "https://cdn.pixabay.com/video/2021/07/22/82399-578640983_large.mp4"
      );
      if (!response.ok) {
        throw new Error("Network response was not ok");
      }
      const blob = await response.blob();

      const video: Video = {
        id: "line-mini-app-initial-dl-test",
        registeredAt: new Date(),
        blob: blob,
      };

      await db.videos.add(video);

      logger.info({ message: "Video downloaded and saved to IndexedDB" });
      setIsOpen(false);
      setVideoUrl(URL.createObjectURL(blob));
    } catch (error) {
      logger.error({ message: "Error downloading or saving video:", error });
    } finally {
      setIsDownloading(false);
    }
  };

  const handleDelete = async () => {
    try {
      await db.videos.delete("line-mini-app-initial-dl-test");
      if (videoUrl != null) {
        URL.revokeObjectURL(videoUrl);
      }
      setVideoUrl(null);
      logger.info({ message: "Video deleted from IndexedDB" });
    } catch (error) {
      logger.error({ message: "Error deleting video from IndexedDB:", error });
    }
  };

  const handleModalOpen = () => {
    setIsOpen(true);
  };

  return (
    <>
      <Helmet>
        <title>{buildAppTitle("LINE ミニアプリ 初期データDLテスト")}</title>
      </Helmet>

      <main className="flex flex-col gap-4 p-4">
        <section className="flex flex-col gap-4">
          <h1 className="text-lg font-semibold">
            LINE Mini App 初期データDLテスト
          </h1>
          <p>
            初回起動時にアプリで使用するデータをダウンロードし、その後のデータ通信量を減らすテストサイトです。
          </p>
        </section>
        <section className="flex flex-col gap-4">
          <h2 className="font-bold">このサイトで検証できること</h2>
          <ol className="list-inside list-decimal space-y-2">
            <li>
              アプリを起動すると、データのダウンロードモーダルが起動します。
            </li>
            <li>
              ダウンロードを実行すると、音声データがブラウザ(IndexedDB)に保存されます。
            </li>
            <li>
              次回起動時には、ブラウザに保存されたデータを使用してアプリが起動します。
            </li>
          </ol>
        </section>

        {videoUrl != null ? (
          <section className="flex flex-col gap-4">
            <h2 className="font-bold">ダウンロードされた動画</h2>
            <p>
              ダウンロードした動画は端末に保存され、次回起動時にデータ通信は発生しません。
            </p>
            <video src={videoUrl} controls className="mx-auto w-full max-w-md">
              お使いのブラウザは動画タグをサポートしていません。
              <track kind="captions" src="captions.vtt" label="Japanese" />
            </video>

            <Button variant="contained" property="error" onClick={handleDelete}>
              データを削除
            </Button>
          </section>
        ) : (
          <section className="flex flex-col gap-4">
            <h2 className="font-bold">初期データのダウンロード</h2>
            <p>アプリを利用開始するには初期データのダウンロードが必要です。</p>
            <Button
              variant="contained"
              property="primary"
              onClick={() => {
                setIsOpen(true);
              }}
            >
              ダウンロード
            </Button>
          </section>
        )}

        <DownloadConfirmModal
          fileSize="42.9MB"
          isOpen={isOpen}
          isDownloading={isDownloading}
          onDownload={handleDownload}
          onCancel={handleModalOpen}
        />
      </main>
    </>
  );
};

動作確認

1. 初回起動時

アプリをはじめて起動すると、動画データをダウンロードするか確認するダイアログが表示されます。

confirm-download-dialog

2. 動画のダウンロード

ダウンロードボタンを押下すると、動画ファイルのダウンロードが開始されます。
downloading

動画を CDN からダウンロードし、Indexed DB に保存します。
※デモに使用した動画はPixabayを利用させていただきました。
downloading-network-tab

3. ダウンロード完了後

ダウンロードした動画を Blob で Video タグの src に指定し、動画が再生可能になります。
donload-complete

4. 2 回目以降の起動時

初回起動値とは違い、Indexed DB に保存された Blob の動画データを取得しているため、CDN とのデータ通信は発生していません。
2nd-open-downloading-network-tab

LINE ミニアプリでもデータの事前DLが可能です

音声や動画などの大容量データを扱うアプリケーションでは、事前にデータをダウンロードしてオフラインでも使用できるネイティブアプリに優位性があると感じていました。
しかし、今回の検証で、LINE ミニアプリでは Service Worker が使えない制約はあるものの、Indexed DB を利用することでネイティブアプリと同じようにデータ通信量とレイテンシーを最適化できることがわかりました。

大容量データを必要とする、ゲームや動画・音声ファイルを扱うサービスを LINE ミニアプリで展開したい場合、Indexed DB で端末にデータをキャッシュする方法を検討してみてはいかがでしょうか。

以上。リテールアプリ共創部のきんじょーでした。

参考

https://iwatendo.hateblo.jp/entry/2018/02/15/215811

https://developer.mozilla.org/ja/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.